package {{package}};

import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.util.Map;

import io.helidon.common.context.Contexts;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.config.Config;
import io.helidon.dbclient.DbClient;
import io.helidon.dbclient.DbExecute;
import io.helidon.dbclient.DbTransaction;
import io.helidon.http.BadRequestException;
import io.helidon.http.NotFoundException;
import io.helidon.http.Status;
import io.helidon.webserver.http.Handler;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;

import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import jakarta.json.JsonReader;
import jakarta.json.JsonValue;

/**
 * An {@link HttpService} that uses {@link DbClient}.
 */
public class PokemonService implements HttpService {

    private static final Logger LOGGER = System.getLogger(PokemonService.class.getName());
    private static final JsonBuilderFactory JSON_FACTORY = Json.createBuilderFactory(Map.of());

    private final DbClient dbClient;
    private final boolean initSchema;
    private final boolean initData;

    /**
     * Create a new Pokémon service with a DB client.
     */
    PokemonService() {
        Config config = Config.global().get("db");
        this.dbClient = Contexts.globalContext()
                .get(DbClient.class)
                .orElseGet(() -> DbClient.create(config));

        initSchema = config.get("init-schema").asBoolean().orElse(true);
        initData = config.get("init-data").asBoolean().orElse(true);
        init();
    }

    private void init() {
        if (initSchema) {
            initSchema();
        }
        if (initData) {
            initData();
        }
    }

    private void initSchema() {
        DbExecute exec = dbClient.execute();
        try {
            exec.namedDml("create-types");
            exec.namedDml("create-pokemons");
        } catch (Exception ex1) {
            LOGGER.log(Level.WARNING, "Could not create tables", ex1);
            try {
                deleteData();
            } catch (Exception ex2) {
                LOGGER.log(Level.WARNING, "Could not delete tables", ex2);
            }
        }
    }

    private void initData() {
        DbTransaction tx = dbClient.transaction();
        try {
            initTypes(tx);
            initPokemons(tx);
            tx.commit();
        } catch (Throwable t) {
            tx.rollback();
            throw t;
        }
    }

    private static void initTypes(DbExecute exec) {
        try (JsonReader reader = Json.createReader(PokemonService.class.getResourceAsStream("/pokemon-types.json"))) {
            JsonArray types = reader.readArray();
            for (JsonValue typeValue : types) {
                JsonObject type = typeValue.asJsonObject();
                exec.namedInsert("insert-type",
                                 type.getInt("id"),
                                 type.getString("name"));
            }
        }
    }

    private static void initPokemons(DbExecute exec) {
        try (JsonReader reader = Json.createReader(PokemonService.class.getResourceAsStream("/pokemons.json"))) {
            JsonArray pokemons = reader.readArray();
            for (JsonValue pokemonValue : pokemons) {
                JsonObject pokemon = pokemonValue.asJsonObject();
                exec.namedInsert("insert-pokemon",
                                 pokemon.getInt("id"),
                                 pokemon.getString("name"),
                                 pokemon.getInt("idType"));
            }
        }
    }

    private void deleteData() {
        DbTransaction tx = dbClient.transaction();
        try {
            tx.namedDelete("delete-all-pokemons");
            tx.namedDelete("delete-all-types");
            tx.commit();
        } catch (Throwable t) {
            tx.rollback();
            throw t;
        }
    }

    @Override
    public void routing(HttpRules rules) {
        rules.get("/", this::index)
                // List all types
                .get("/type", this::listTypes)
                // List all Pokémon
                .get("/pokemon", this::listPokemons)
                // Get Pokémon by name
                .get("/pokemon/name/{name}", this::getPokemonByName)
                // Get Pokémon by ID
                .get("/pokemon/{id}", this::getPokemonById)
                // Create new Pokémon
                .post("/pokemon", Handler.create(Pokemon.class, this::insertPokemon))
                // Update name of existing Pokémon
                .put("/pokemon", Handler.create(Pokemon.class, this::updatePokemon))
                // Delete Pokémon by ID including type relation
                .delete("/pokemon/{id}", this::deletePokemonById);
    }

    /**
     * Return index page.
     *
     * @param request  the server request
     * @param response the server response
     */
    private void index(ServerRequest request, ServerResponse response) {
        response.headers().contentType(MediaTypes.TEXT_PLAIN);
        response.send("""
                              Pokemon JDBC Example:
                                   GET /type                - List all pokemon types
                                   GET /pokemon             - List all pokemons
                                   GET /pokemon/{id}        - Get pokemon by id
                                   GET /pokemon/name/{name} - Get pokemon by name
                                   POST /pokemon            - Insert new pokemon:
                                                              {"id":<id>,"name":<name>,"type":<type>}
                                   PUT /pokemon             - Update pokemon
                                                              {"id":<id>,"name":<name>,"type":<type>}
                                   DELETE /pokemon/{id}     - Delete pokemon with specified id
                              """);
    }

    /**
     * Return JsonArray with all stored Pokémon.
     * Pokémon object contains list of all type names.
     * This method is abstract because implementation is DB dependent.
     *
     * @param request  the server request
     * @param response the server response
     */
    private void listTypes(ServerRequest request, ServerResponse response) {
        JsonArray jsonArray = dbClient.execute()
                .namedQuery("select-all-types")
                .map(row -> row.as(JsonObject.class))
                .collect(JSON_FACTORY::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll)
                .build();
        response.send(jsonArray);
    }

    /**
     * Return JsonArray with all stored Pokémon.
     * Pokémon object contains list of all type names.
     * This method is abstract because implementation is DB dependent.
     *
     * @param request  the server request
     * @param response the server response
     */
    private void listPokemons(ServerRequest request, ServerResponse response) {
        JsonArray jsonArray = dbClient.execute().namedQuery("select-all-pokemons")
                .map(row -> row.as(JsonObject.class))
                .collect(JSON_FACTORY::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::addAll)
                .build();
        response.send(jsonArray);
    }

    /**
     * Get a single Pokémon by id.
     *
     * @param request  server request
     * @param response server response
     */
    private void getPokemonById(ServerRequest request, ServerResponse response) {
        int pokemonId = Integer.parseInt(request.path()
                                                 .pathParameters()
                                                 .get("id"));

        response.send(dbClient.execute().createNamedGet("select-pokemon-by-id")
                              .addParam("id", pokemonId)
                              .execute()
                              .orElseThrow(() -> new NotFoundException("Pokemon " + pokemonId + " not found"))
                              .as(JsonObject.class));
    }

    /**
     * Get a single Pokémon by name.
     *
     * @param request  server request
     * @param response server response
     */
    private void getPokemonByName(ServerRequest request, ServerResponse response) {
        String pokemonName = request.path().pathParameters().get("name");
        response.send(dbClient.execute().namedGet("select-pokemon-by-name", pokemonName)
                              .orElseThrow(() -> new NotFoundException("Pokemon " + pokemonName + " not found"))
                              .as(JsonObject.class));
    }

    /**
     * Insert new Pokémon with specified name.
     *
     * @param pokemon  request entity
     * @param response the server response
     */
    private void insertPokemon(Pokemon pokemon, ServerResponse response) {
        long count = dbClient.execute().createNamedInsert("insert-pokemon")
                .indexedParam(pokemon)
                .execute();
        response.status(Status.CREATED_201)
                .send("Inserted: " + count + " values\n");
    }

    /**
     * Update a Pokémon.
     * Uses a transaction.
     *
     * @param pokemon  request entity
     * @param response the server response
     */
    private void updatePokemon(Pokemon pokemon, ServerResponse response) {
        long count = dbClient.execute().createNamedUpdate("update-pokemon-by-id")
                .namedParam(pokemon)
                .execute();
        response.send("Updated: " + count + " values\n");
    }

    /**
     * Delete Pokémon with specified id (key).
     *
     * @param request  the server request
     * @param response the server response
     */
    private void deletePokemonById(ServerRequest request, ServerResponse response) {
        int id = request.path()
                .pathParameters()
                .first("id").map(Integer::parseInt)
                .orElseThrow(() -> new BadRequestException("No pokemon id"));
        dbClient.execute().createNamedDelete("delete-pokemon-by-id")
                .addParam("id", id)
                .execute();
        response.status(Status.NO_CONTENT_204)
                .send();
    }
}
